#include <ts7200.h>
#include <bwio.h>
#include <user.h>

// message/reply types
#define SIGNUP 0
#define PLAY 1
#define QUIT 2
#define BAD 3

// SIGNUP responses
#define SIGNUP_ERROR_GAME_FULL -1
#define SIGNUP_OK 0

// PLAY moves
#define ROCK 0
#define PAPER 1
#define SCISSORS 2

// PLAY responses
#define PLAY_ERROR_NOT_ALLOWED -1
#define PLAY_WIN 0
#define PLAY_DRAW 1
#define PLAY_LOSE 2
#define PLAY_OTHER_QUIT 3

// QUIT responses
#define QUIT_ERROR_NOT_PLAYING -1
#define QUIT_OK 0

// BAD responses
#define BAD_ERROR_INVALID_MESSAGE -1

// states
#define RPS_START 0
#define RPS_SIGNUP_ONE 1
#define RPS_SIGNUP_BOTH 2
#define RPS_PLAY_ONE 3

typedef struct __rpsmsg {
  int type;
  int move;   // used only for PLAY messages
} rpsmsg;

typedef struct __rpsreply {
  int type;
  int response;
} rpsreply;

void NameServer();

/**
 * Returns the result for player 1.
 */
int RpsJudge( int move_1, int move_2 ) {
  if ( move_1 == move_2 ) return PLAY_DRAW;
  if ( (move_1-move_2+3)%3 == 1 ) return PLAY_WIN;
  return PLAY_LOSE;
}

/**
 * Prints the result of a game and blocks until a key is pressed.
 */
void RpsPrintGame( int client_1, int client_2, int move_1, int move_2 ) {
  bwprintf( COM2, "[%d] %s vs. [%d] %s\r\n",
      client_1, (move_1 == ROCK) ? "rock" :
          (move_1 == PAPER) ? "paper" : "scissors",
      client_2, (move_2 == ROCK) ? "rock" :
          (move_2 == PAPER) ? "paper" : "scissors" );
  bwprintf( COM2, "press any key to continue...\r\n" );
  bwgetc( COM2 );
}

void RpsServer() {
  int server_tid = MyTid();
  bwprintf( COM2, "[%d] new server started\r\n", server_tid );
  RegisterAs( "rps-server" );
  rpsmsg msg;
  rpsreply reply;
  int tid, msg_len;

  int client_1 = -1, client_2 = -1;
  int client_1_move = -1, client_2_move = -1;
  int server_state = RPS_START;

  for ( ; ; ) {
    msg_len = Receive( &tid, (char*)( &msg ), sizeof(rpsmsg) );
    
    // check for bad requests
    reply.type = BAD;
    reply.response = BAD_ERROR_INVALID_MESSAGE;
    if ( msg_len != sizeof(rpsmsg) ) {
      bwprintf( COM2, "[%d] ERROR: received malformed message from %d!\r\n",
          server_tid, tid );
      Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
      continue;
    } 
    if ( msg.type == PLAY && (msg.move < ROCK || msg.move > SCISSORS) ) {
      bwprintf( COM2, "[%d] ERROR: unknown PLAY move %d, ignoring!\r\n",
          server_tid, msg.move );
      Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
      continue;
    }

    // main state machine
    reply.type = msg.type;
    if ( server_state == RPS_START ) {
      if ( msg.type == SIGNUP ) {
        // set first player
        client_1 = tid;
        server_state = RPS_SIGNUP_ONE;
      } else if ( msg.type == PLAY ) {
        // players must sign up before playing!
        reply.response = PLAY_ERROR_NOT_ALLOWED;
        Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
      } else if ( msg.type == QUIT ) {
        // players must sign up to quit!
        reply.response = QUIT_ERROR_NOT_PLAYING;
        Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
      }
    } else if ( server_state == RPS_SIGNUP_ONE ) {
      if ( msg.type == SIGNUP ) {
        // set second player
        client_2 = tid;
        reply.response = SIGNUP_OK;
        Reply( client_1, (char*)( &reply ), sizeof(rpsreply) );
        Reply( client_2, (char*)( &reply ), sizeof(rpsreply) );
        server_state = RPS_SIGNUP_BOTH;
      } else if ( msg.type == PLAY ) {
        // players must sign up to play!
        if ( tid == client_1 ) {
          // other player quit
          reply.response = PLAY_OTHER_QUIT;
        } else {
          reply.response = PLAY_ERROR_NOT_ALLOWED;
        }
        Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
      } else if ( msg.type == QUIT ) {
        if ( tid == client_1 ) {
          reply.response = QUIT_OK;
          server_state = RPS_START;
        } else {
          // players must sign up to quit!
          reply.response = QUIT_ERROR_NOT_PLAYING;
        }
        Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
      }
    } else if ( server_state == RPS_SIGNUP_BOTH ) {
      if ( msg.type == SIGNUP ) {
        // game is full!
        reply.response = SIGNUP_ERROR_GAME_FULL;
        Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
      } else if ( msg.type == PLAY ) {
        if ( tid == client_1 ) {
          // player 1 moved
          client_1_move = msg.move;
          server_state = RPS_PLAY_ONE;
        } else if ( tid == client_2 ) {
          // player 2 moved
          client_2_move = msg.move;
          server_state = RPS_PLAY_ONE;
        } else {
          // players must sign up to play!
          reply.response = PLAY_ERROR_NOT_ALLOWED;
          Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
        }
      } else if ( msg.type == QUIT ) {
        if ( tid == client_1 || tid == client_2 ) {
          reply.response = QUIT_OK;
          Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
          if ( tid == client_1 )
            client_1 = client_2;
          server_state = RPS_SIGNUP_ONE;
        } else {
          // players must sign up to quit!
          reply.response = QUIT_ERROR_NOT_PLAYING;
          Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
        }
      }
    } else if ( server_state == RPS_PLAY_ONE ) {
      if ( msg.type == SIGNUP ) {
        // game is full!
        reply.response = SIGNUP_ERROR_GAME_FULL;
        Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
      } else if ( msg.type == PLAY ) {
        if ( tid == client_1 || tid == client_2 ) {
          // player 1 moved
          if ( tid == client_1 )
            client_1_move = msg.move;
          else
            client_2_move = msg.move;
          reply.response = RpsJudge( client_1_move, client_2_move );
          Reply( client_1, (char*)( &reply ), sizeof(rpsreply) );
          reply.response = RpsJudge( client_2_move, client_1_move );
          Reply( client_2, (char*)( &reply ), sizeof(rpsreply) );
          RpsPrintGame( client_1, client_2, client_1_move, client_2_move );
          server_state = RPS_SIGNUP_BOTH;
        } else {
          // players must sign up to play!
          reply.response = PLAY_ERROR_NOT_ALLOWED;
          Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
        }
      } else if ( msg.type == QUIT ) { 
        if ( tid == client_1 || tid == client_2 ) {
          reply.response = QUIT_OK;
          Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
          if ( tid == client_2 )
            client_1 = client_2;
          server_state = RPS_SIGNUP_ONE;
        } else {
          // players must sign up to quit!
          reply.response = QUIT_ERROR_NOT_PLAYING;
          Reply( tid, (char*)( &reply ), sizeof(rpsreply) );
        }
      }
    } else {
      bwprintf( COM2, "[%d] ERROR: unknown message type %d, ignoring!\r\n",
          server_tid, msg.type );
    }
  }
}

/**
 * RpsClient() plays by the rules: it signs up, makes a certain number of
 * plays, and finally quits. It also quits if it discovers that its partner
 * has left, and makes a reasonable attempt to detect errors. If it detect
 * errors from which it cannot recover, it exits.
 */
void RpsClient() {
  int tid = MyTid();
  bwprintf( COM2, "[%d] new client started\r\n", tid );
  int server_tid = WhoIs( "rps-server" );
  bwprintf( COM2, "[%d] found server at %d\r\n", tid, server_tid );

  rpsmsg msg;
  rpsreply reply;
  int reply_len;
  
  // sign up
  msg.type = SIGNUP;
  reply_len = Send(
      server_tid, (char*)( &msg ), sizeof(rpsmsg),
      (char*)( &reply ), sizeof(rpsreply) );
  if (reply_len != sizeof(rpsreply)) {
    bwprintf( COM2, "[%d] ERROR: signup failed!\r\n", tid );
    Exit();
  } else {
    switch (reply.response) {
    case SIGNUP_OK:
      bwprintf( COM2, "[%d] signup ok\r\n", tid );
      break;
    case SIGNUP_ERROR_GAME_FULL:
      bwprintf( COM2, "[%d] ERROR: game full!\r\n", tid );
      Exit();
      break;
    }
  }
  
  int i;
  for (i = 0; i < tid; i++) {
    msg.type = PLAY;
    msg.move = ((tid+1)*i+tid)%3;
    reply_len = Send(
        server_tid, (char*)( &msg ), sizeof(rpsmsg),
        (char*)( &reply ), sizeof(rpsreply) );
    if (reply_len != sizeof(rpsreply)) {
      bwprintf( COM2, "[%d] ERROR: play failed!\r\n", tid );
      Exit();
    } else {
      if ( reply.response == PLAY_WIN ) {
        bwprintf( COM2, "[%d] i win :)\r\n", tid );
      } else if ( reply.response == PLAY_DRAW ) {
        bwprintf( COM2, "[%d] a tie :|\r\n", tid );
      } else if ( reply.response == PLAY_LOSE ) {
        bwprintf( COM2, "[%d] i lose :(\r\n", tid );
      } else if ( reply.response == PLAY_ERROR_NOT_ALLOWED ) {
        bwprintf( COM2, "[%d] ERROR: can't play right now!\r\n", tid );
      } else if ( reply.response == PLAY_OTHER_QUIT ) {
        bwprintf( COM2, "[%d] hey! my partner quit on me! x-(\r\n", tid );
        break;
      }
    }
  }
  // quit
  msg.type = QUIT;
  reply_len = Send(
      server_tid, (char*)( &msg ), sizeof(rpsmsg),
      (char*)( &reply ), sizeof(rpsreply) );
  if (reply_len != sizeof(rpsreply)) {
    bwprintf( COM2, "[%d] ERROR: quit failed!\r\n", tid );
  } else {
    switch (reply.response) {
    case QUIT_OK:
      bwprintf( COM2, "[%d] bye.\r\n", tid );
      break;
    case QUIT_ERROR_NOT_PLAYING:
      bwprintf( COM2, "[%d] ERROR: can't quit - i'm not playing!\r\n", tid );
      break;
    }
  }
  Exit();
}

/**
 * RpsHacker() attempts to subvert the server. It shows up after the game
 * is full, sends invalid messages, and tries sending valid (but meaningless)
 * PLAY and QUIT requests.
 */
void RpsHacker() {
  int tid = MyTid();
  bwprintf( COM2, "[%d] new hacker started\r\n", tid );
  int server_tid = WhoIs( "rps-server" );
  bwprintf( COM2, "[%d] found server at %d\r\n", tid, server_tid );

  rpsmsg msg;
  rpsreply reply;
  int reply_len;
 
  // attempt invalid signup - slots should already be filled
  msg.type = SIGNUP;
  reply_len = Send(
      server_tid, (char*)( &msg ), sizeof(rpsmsg),
      (char*)( &reply ), sizeof(rpsreply) );
  if (reply_len != sizeof(rpsreply)) {
    bwprintf( COM2, "[%d] ERROR: signup failed!\r\n", tid );
  } else {
    switch (reply.response) {
    case SIGNUP_OK:
      bwprintf( COM2, "[%d] signup ok\r\n", tid );
      break;
    case SIGNUP_ERROR_GAME_FULL:
      bwprintf( COM2, "[%d] ERROR: game full!\r\n", tid );
      break;
    }
  }

  // attempt sending garbage - these should all be ignored
  msg.type = 0xdeadbeef;
  msg.move = 0xcafedada;
  Send( server_tid, (char*)( &msg ), 3, (char*)( &reply ), 3 );
  Send( server_tid, (char*)( &msg ), 12, (char*)( &reply ), 3 );
  Send( server_tid, (char*)( &msg ), 12, (char*)( &reply ), 12 );
  Send( server_tid, (char*)( &msg ), 3, (char*)( &reply ), 12 );

  // attempt sending invalid PLAY - move should be flagged as invalid
  msg.type = PLAY;
  Send( server_tid, (char*)( &msg ), sizeof(rpsmsg),
      (char*)( &reply ), sizeof(rpsreply) );
  
  // attempt sending PLAY and QUIT requests - these should return 
  // suitable errors to the client
  msg.move = PAPER;
  reply_len = Send(
      server_tid, (char*)( &msg ), sizeof(rpsmsg),
      (char*)( &reply ), sizeof(rpsreply) );
  if (reply_len != sizeof(rpsreply)) {
    bwprintf( COM2, "[%d] ERROR: play failed!\r\n", tid );
  } else {
    if ( reply.response == PLAY_WIN ) {
      bwprintf( COM2, "[%d] i win :)\r\n", tid );
    } else if ( reply.response == PLAY_DRAW ) {
      bwprintf( COM2, "[%d] a tie :|\r\n", tid );
    } else if ( reply.response == PLAY_LOSE ) {
      bwprintf( COM2, "[%d] i lose :(\r\n", tid );
    } else if ( reply.response == PLAY_ERROR_NOT_ALLOWED ) {
      bwprintf( COM2, "[%d] ERROR: can't play right now!\r\n", tid );
    } else if ( reply.response == PLAY_OTHER_QUIT ) {
      bwprintf( COM2, "[%d] hey! my partner quit on me! x-(\r\n", tid );
    }
  }
 
  msg.type = QUIT;
  reply_len = Send(
      server_tid, (char*)( &msg ), sizeof(rpsmsg),
      (char*)( &reply ), sizeof(rpsreply) );
  if (reply_len != sizeof(rpsreply)) {
    bwprintf( COM2, "[%d] ERROR: quit failed!\r\n", tid );
  } else {
    switch (reply.response) {
    case QUIT_OK:
      bwprintf( COM2, "[%d] bye.\r\n", tid );
      break;
    case QUIT_ERROR_NOT_PLAYING:
      bwprintf( COM2, "[%d] ERROR: can't quit - i'm not playing!\r\n", tid );
      break;
    }
  }
  Exit();
}

void UserInit() {
  // turn off COM2 FIFOs; otherwise, bwgetc() blocks until FIFO is full,
  // which is not what we want!
  bwsetfifo( COM2, OFF );

  // create name server and game server/clients
  Create( 4, &NameServer );
  Create( 3, &RpsServer );
  Create( 4, &RpsClient );
  Create( 1, &RpsClient );
  Create( 1, &RpsHacker );
  Exit();
}
